<template>
  <div :style="style" :class="{draggable: draggable, resizable: resizable, active: enabled, dragging: dragging, resizing: resizing}" class="vdr" @mousedown="elmDown" @touchstart.stop="elmDown" @dblclick="fillParent" @keydown.stop.prevent="handleKeyDown">
    <template v-for="(item, index) in handles">
      <div v-if="resizable" :key="index" :class="'handle-' + item" :style="{ display: enabled ? 'block' : 'none'}" class="handle" @mousedown.stop.prevent="handleDown(item, $event)" @touchstart.stop.prevent="handleDown(item, $event)"></div>
    </template>
    <slot></slot>
  </div>
</template>

<script>
import { matchesSelectorToParentElements } from "../utils/dom"; // 将选择器与父元素匹配

export default {
  replace: true,
  name: "VueDraggableResizable",
  props: {
    active: {
      type: Boolean,
      default: false
    },
    draggable: {
      type: Boolean,
      default: true
    },
    resizable: {
      type: Boolean,
      default: true
    },
    w: {
      type: Number,
      default: 200,
      validator: function(val) {
        return val > 0;
      }
    },
    h: {
      type: Number,
      default: 200,
      validator: function(val) {
        return val > 0;
      }
    },
    minw: {
      type: Number,
      default: 50,
      validator: function(val) {
        return val >= 0;
      }
    },
    minh: {
      type: Number,
      default: 50,
      validator: function(val) {
        return val >= 0;
      }
    },
    x: {
      type: Number,
      default: 0,
      validator: function(val) {
        return typeof val === "number";
      }
    },
    y: {
      type: Number,
      default: 0,
      validator: function(val) {
        return typeof val === "number";
      }
    },
    z: {
      type: [String, Number],
      default: "auto",
      validator: function(val) {
        const valid = typeof val === "string" ? val === "auto" : val >= 0;
        return valid;
      }
    },
    handles: {
      type: Array,
      default: function() {
        return ["tl", "tm", "tr", "mr", "br", "bm", "bl", "ml"];
      },
      validator: function(val) {
        var s = new Set(["tl", "tm", "tr", "mr", "br", "bm", "bl", "ml"]);

        return new Set(val.filter(h => s.has(h))).size === val.length;
      }
    },
    dragHandle: {
      type: String,
      default: null
    },
    dragCancel: {
      type: String,
      default: null
    },
    axis: {
      type: String,
      default: "both",
      validator: function(val) {
        return ["x", "y", "both"].indexOf(val) !== -1;
      }
    },
    grid: {
      type: Array,
      default: function() {
        return [1, 1];
      }
    },
    parent: {
      type: Boolean,
      default: false
    },
    maximize: {
      type: Boolean,
      default: false
    },
    // 定义组件是否开启冲突检测
    isConflictCheck: {
      type: Boolean,
      default: false
    },
    // 是否开启元素对齐
    snap: {
      type: Boolean,
      default: false
    },
    // 当调用对齐时，用来设置组件与组件之间的对齐距离，以像素为单位。
    snapTolerance: {
      type: Number,
      default: 5,
      validator: function(val) {
        return typeof val === "number";
      }
    }
  },

  data: function() {
    return {
      top: this.y,
      left: this.x,
      width: this.w,
      height: this.h,
      resizing: false,
      dragging: false,
      enabled: this.active,
      handle: null,
      zIndex: this.z,
      // 如果组件之间存在冲突，用来记录原位置信息
      restoreY: 0,
      restoreX: 0,
      restoreW: 0,
      restoreH: 0
    };
  },

  computed: {
    style: function() {
      return {
        top: this.top + "px",
        left: this.left + "px",
        width: this.width + "px",
        height: this.height + "px",
        zIndex: this.zIndex
      };
    }
  },

  watch: {
    active: function(val) {
      this.enabled = val;
    },
    z: function(val) {
      if (val >= 0 || val === "auto") {
        this.zIndex = val;
      }
    },
    y: function(val) {
      if (val >= 0) {
        this.recording();
        this.top = val;
        this.conflictCheck();
        this.reviewDimensions();
      }
    },
    x: function(val) {
      if (val >= 0) {
        this.recording();
        this.left = val;
        this.conflictCheck();
        this.reviewDimensions();
      }
    },
    w: function(val) {
      if (val >= 0) {
        this.recording();
        this.width = val;
        this.conflictCheck();
        this.reviewDimensions();
      }
    },
    h: function(val) {
      if (val >= 0) {
        this.recording();
        this.height = val;
        this.conflictCheck();
        this.reviewDimensions();
      }
    },
    left: function(val) {
      this.$emit("update:x", parseInt(val));
    },
    top: function(val) {
      this.$emit("update:y", parseInt(val));
    },
    width: function(val) {
      this.$emit("update:w", parseInt(val));
    },
    height: function(val) {
      this.$emit("update:h", parseInt(val));
    }
  },

  created: function() {
    this.parentX = 0;
    this.parentW = 9999;
    this.parentY = 0;
    this.parentH = 9999;

    this.mouseX = 0;
    this.mouseY = 0;

    this.lastMouseX = 0;
    this.lastMouseY = 0;

    this.mouseOffX = 0;
    this.mouseOffY = 0;

    this.elmX = 0;
    this.elmY = 0;

    this.elmW = 0;
    this.elmH = 0;
  },
  mounted: function() {
    document.documentElement.addEventListener("mousemove", this.handleMove, true);
    document.documentElement.addEventListener("mousedown", this.deselect, true);
    document.documentElement.addEventListener("mouseup", this.handleUp, true);

    // touch events bindings
    document.documentElement.addEventListener("touchmove", this.handleMove, true);
    document.documentElement.addEventListener("touchend touchcancel", this.deselect, true);
    document.documentElement.addEventListener("touchstart", this.handleUp, true);

    this.setSnap();
    this.setConflictCheck();
    this.getElmPosition();

    this.reviewDimensions();
  },
  beforeUnmount: function() {
    document.documentElement.removeEventListener("mousemove", this.handleMove, true);
    document.documentElement.removeEventListener("mousedown", this.deselect, true);
    document.documentElement.removeEventListener("mouseup", this.handleUp, true);

    // touch events bindings removed
    document.documentElement.removeEventListener("touchmove", this.handleMove, true);
    document.documentElement.removeEventListener("touchend touchcancel", this.deselect, true);
    document.documentElement.removeEventListener("touchstart", this.handleUp, true);
  },

  methods: {
    // 设置对齐元素
    setSnap: function() {
      if (this.snap) {
        this.$el.setAttribute("data-is-snap", "true");
      } else {
        this.$el.setAttribute("data-is-snap", "false");
      }
    },
    // 检测对齐元素
    snapCheck: function() {
      if (this.snap) {
        const p = this.$el.parentNode.childNodes; // 获取当前父节点下所有子节点
        if (p.length > 1) {
          const x1 = this.left;
          const x2 = this.left + this.width;
          const y1 = this.top;
          const y2 = this.top + this.height;
          for (let i = 0; i < p.length; i++) {
            if (p[i] !== this.$el && p[i].className !== undefined && p[i].getAttribute("data-is-snap") !== "false") {
              const l = p[i].offsetLeft; // 对齐目标的left
              const r = l + p[i].offsetWidth; // 对齐目标右侧距离窗口的left
              const t = p[i].offsetTop; // 对齐目标的top
              const b = t + p[i].offsetHeight; // 对齐目标右侧距离窗口的top

              const ts = Math.abs(t - y2) <= this.snapTolerance;
              const bs = Math.abs(b - y1) <= this.snapTolerance;
              const ls = Math.abs(l - x2) <= this.snapTolerance;
              const rs = Math.abs(r - x1) <= this.snapTolerance;
              if (ts) {
                this.top = t - this.height;
              }
              if (bs) {
                this.top = b;
              }
              if (ls) {
                this.left = l - this.width;
              }
              if (rs) {
                this.left = r;
              }
            }
          }
        }
      }
    },
    // 获得激活组件的位置信息
    getElmPosition: function() {
      this.elmX = parseInt(this.$el.style.left);
      this.elmY = parseInt(this.$el.style.top);
      this.elmW = this.$el.offsetWidth || this.$el.clientWidth;
      this.elmH = this.$el.offsetHeight || this.$el.clientHeight;
    },
    // 设置冲突检测
    setConflictCheck: function() {
      if (this.isConflictCheck) {
        this.$el.setAttribute("data-is-check", "true");
      } else {
        this.$el.setAttribute("data-is-check", "false");
      }
    },
    // 冲突检测
    conflictCheck: function() {
      if (this.isConflictCheck) {
        const p = this.$el.parentNode.childNodes; // 获取当前父节点下所有子节点
        if (p.length > 1) {
          for (let i = 0; i < p.length; i++) {
            if (p[i] !== this.$el && p[i].className !== undefined && p[i].getAttribute("data-is-check") !== "false") {
              const tw = p[i].offsetWidth;
              const th = p[i].offsetHeight;
              const tl = p[i].offsetLeft;
              const tt = p[i].offsetTop;
              // 如果冲突，就将回退到移动前的位置
              if (
                (this.top >= tt && this.left >= tl && tt + th > this.top && tl + tw > this.left) ||
                (this.top <= tt && this.left < tl && this.top + this.height > tt && this.left + this.width > tl)
              ) {
                // 左上角与右下角重叠
                this.top = this.restoreY;
                this.left = this.restoreX;
                this.width = this.restoreW;
                this.height = this.restoreH;
              } else if (
                (this.left <= tl && this.top >= tt && this.left + this.width > tl && this.top < tt + th) ||
                (this.top < tt && this.left > tl && this.top + this.height > tt && this.left < tl + tw)
              ) {
                // 右上角与左下角重叠
                this.top = this.restoreY;
                this.left = this.restoreX;
                this.width = this.restoreW;
                this.height = this.restoreH;
              } else if (
                (this.top < tt && this.left <= tl && this.top + this.height > tt && this.left + this.width > tl) ||
                (this.top > tt && this.left >= tl && this.top < tt + th && this.left < tl + tw)
              ) {
                // 下边与上边重叠
                this.top = this.restoreY;
                this.left = this.restoreX;
                this.width = this.restoreW;
                this.height = this.restoreH;
              } else if (
                (this.top <= tt && this.left >= tl && this.top + this.height > tt && this.left < tl + tw) ||
                (this.top >= tt && this.left <= tl && this.top < tt + th && this.left > tl + tw)
              ) {
                // 上边与下边重叠（宽度不一样
                this.top = this.restoreY;
                this.left = this.restoreX;
                this.width = this.restoreW;
                this.height = this.restoreH;
              } else if (
                (this.left >= tl && this.top >= tt && this.left < tl + tw && this.top < tt + th) ||
                (this.top > tt && this.left <= tl && this.left + this.width > tl && this.top < tt + th)
              ) {
                // 左边与右边重叠
                this.top = this.restoreY;
                this.left = this.restoreX;
                this.width = this.restoreW;
                this.height = this.restoreH;
              } else if (
                (this.top <= tt && this.left >= tl && this.top + this.height > tt && this.left < tl + tw) ||
                (this.top >= tt && this.left <= tl && this.top < tt + th && this.left + this.width > tl)
              ) {
                // 左边与右边重叠（高度不一样）
                this.top = this.restoreY;
                this.left = this.restoreX;
                this.width = this.restoreW;
                this.height = this.restoreH;
              }
            }
          }
        }
      }
    },
    // 检测尺寸
    reviewDimensions: function() {
      // if (this.minw > this.w) this.width = this.minw;
      // if (this.minh > this.h) this.height = this.minh;
      // if (this.parent) {
      //   const parentW = parseInt(this.$el.parentNode.clientWidth, 10);
      //   const parentH = parseInt(this.$el.parentNode.clientHeight, 10);
      //   this.parentW = parentW;
      //   this.parentH = parentH;
      //   if (this.w > this.parentW) this.width = parentW;
      //   if (this.h > this.parentH) this.height = parentH;
      //   if (this.x + this.w > this.parentW) this.width = parentW - this.x;
      //   if (this.y + this.h > this.parentH) this.height = parentH - this.y;
      // }
      // this.elmW = this.width;
      // this.elmH = this.height;
      // this.$emit("resizing", this.left, this.top, this.width, this.height);
    },
    // 鼠标激活当前组件
    elmDown: function(e) {
      const target = e.target || e.srcElement;
      if (this.$el.contains(target)) {
        target.tabIndex = 0;
        target.focus();
        if (
          (this.dragHandle && !matchesSelectorToParentElements(target, this.dragHandle, this.$el)) ||
          (this.dragCancel && matchesSelectorToParentElements(target, this.dragCancel, this.$el))
        ) {
          return;
        }

        e.stopPropagation();
        e.preventDefault();

        this.reviewDimensions();

        if (!this.enabled) {
          this.enabled = true;

          this.$emit("activated");
          this.$emit("update:active", true);
        }
        // 激活区域块时获取当前区域块的X,Y,W,H
        this.getElmPosition();
        if (this.draggable) {
          this.dragging = true;
          this.recording();
        }
      }
    },
    // 取消激活
    deselect: function(e) {
      if (e.type.indexOf("touch") !== -1) {
        this.mouseX = e.changedTouches[0].clientX;
        this.mouseY = e.changedTouches[0].clientY;
      } else {
        this.mouseX = e.pageX || e.clientX + document.documentElement.scrollLeft;
        this.mouseY = e.pageY || e.clientY + document.documentElement.scrollTop;
      }

      this.lastMouseX = this.mouseX;
      this.lastMouseY = this.mouseY;

      const target = e.target || e.srcElement;
      const regex = new RegExp("handle-([trmbl]{2})", "");

      if (!this.$el.contains(target) && !regex.test(target.className)) {
        if (this.enabled) {
          this.enabled = false;

          this.$emit("deactivated");
          this.$emit("update:active", false);
        }
      }
    },
    // 鼠标按下控制点
    handleDown: function(handle, e) {
      this.handle = handle;

      if (e.stopPropagation) e.stopPropagation();
      if (e.preventDefault) e.preventDefault();

      // 当区域块处于被激活状态时，手柄被点击时获取当前区域块的X,Y,W,H（如果不加进来，会出现区域块会跳动到回退前的位置）
      this.getElmPosition();

      this.recording();
      // END
      this.resizing = true;
    },
    // 最大化
    fillParent: function(e) {
      if (!this.parent || !this.resizable || !this.maximize) return;

      let done = false;

      const animate = () => {
        if (!done) {
          window.requestAnimationFrame(animate);
        }

        if (this.axis === "x") {
          if (this.width === this.parentW && this.left === this.parentX) {
            done = true;
          }
        } else if (this.axis === "y") {
          if (this.height === this.parentH && this.top === this.parentY) {
            done = true;
          }
        } else if (this.axis === "both") {
          if (this.width === this.parentW && this.height === this.parentH && this.top === this.parentY && this.left === this.parentX) {
            done = true;
          }
        }

        if (this.axis === "x" || this.axis === "both") {
          if (this.width < this.parentW) {
            this.width++;
            this.elmW++;
          }

          if (this.left > this.parentX) {
            this.left--;
            this.elmX--;
          }
        }

        if (this.axis === "y" || this.axis === "both") {
          if (this.height < this.parentH) {
            this.height++;
            this.elmH++;
          }

          if (this.top > this.parentY) {
            this.top--;
            this.elmY--;
          }
        }

        this.$emit("resizing", this.left, this.top, this.width, this.height);
      };

      window.requestAnimationFrame(animate);
    },
    // 鼠标移动
    handleMove: function(e) {
      const isTouchMove = e.type.indexOf("touchmove") !== -1;
      this.mouseX = isTouchMove ? e.touches[0].clientX : e.pageX || e.clientX + document.documentElement.scrollLeft;
      this.mouseY = isTouchMove ? e.touches[0].clientY : e.pageY || e.clientY + document.documentElement.scrollTop;

      let diffX = this.mouseX - this.lastMouseX + this.mouseOffX;
      let diffY = this.mouseY - this.lastMouseY + this.mouseOffY;

      this.mouseOffX = this.mouseOffY = 0;

      this.lastMouseX = this.mouseX;
      this.lastMouseY = this.mouseY;

      const dX = diffX;
      const dY = diffY;

      if (this.resizing) {
        if (this.handle.indexOf("t") >= 0) {
          if (this.elmH - dY < this.minh) {
            this.mouseOffY = dY - (diffY = this.elmH - this.minh);
          } else if (this.parent && this.elmY + dY < this.parentY) {
            this.mouseOffY = dY - (diffY = this.parentY - this.elmY);
          }
          this.elmY += diffY;
          this.elmH -= diffY;
        }

        if (this.handle.indexOf("b") >= 0) {
          if (this.elmH + dY < this.minh) {
            this.mouseOffY = dY - (diffY = this.minh - this.elmH);
          } else if (this.parent && this.elmY + this.elmH + dY > this.parentH) {
            this.mouseOffY = dY - (diffY = this.parentH - this.elmY - this.elmH);
          }
          this.elmH += diffY;
        }

        if (this.handle.indexOf("l") >= 0) {
          if (this.elmW - dX < this.minw) {
            this.mouseOffX = dX - (diffX = this.elmW - this.minw);
          } else if (this.parent && this.elmX + dX < this.parentX) {
            this.mouseOffX = dX - (diffX = this.parentX - this.elmX);
          }
          this.elmX += diffX;
          this.elmW -= diffX;
        }

        if (this.handle.indexOf("r") >= 0) {
          if (this.elmW + dX < this.minw) {
            this.mouseOffX = dX - (diffX = this.minw - this.elmW);
          } else if (this.parent && this.elmX + this.elmW + dX > this.parentW) {
            this.mouseOffX = dX - (diffX = this.parentW - this.elmX - this.elmW);
            this.elmW += diffX;
          }
          this.elmW += diffX;
        }

        this.left = Math.round(this.elmX / this.grid[0]) * this.grid[0];
        this.top = Math.round(this.elmY / this.grid[1]) * this.grid[1];

        this.width = Math.round(this.elmW / this.grid[0]) * this.grid[0];
        this.height = Math.round(this.elmH / this.grid[1]) * this.grid[1];

        this.$emit("resizing", this.left, this.top, this.width, this.height);
      } else if (this.dragging) {
        if (this.parent) {
          if (this.elmX + dX < this.parentX) {
            this.mouseOffX = dX - (diffX = this.parentX - this.elmX);
          } else if (this.elmX + this.elmW + dX > this.parentW) {
            this.mouseOffX = dX - (diffX = this.parentW - this.elmX - this.elmW);
          }

          if (this.elmY + dY < this.parentY) {
            this.mouseOffY = dY - (diffY = this.parentY - this.elmY);
          } else if (this.elmY + this.elmH + dY > this.parentH) {
            this.mouseOffY = dY - (diffY = this.parentH - this.elmY - this.elmH);
          }
        }

        this.elmX += diffX;
        this.elmY += diffY;

        if (this.axis === "x" || this.axis === "both") {
          this.left = Math.round(this.elmX / this.grid[0]) * this.grid[0];
        }
        if (this.axis === "y" || this.axis === "both") {
          this.top = Math.round(this.elmY / this.grid[1]) * this.grid[1];
        }
        this.snapCheck();
        this.$emit("dragging", this.left, this.top);
      }
    },
    // 键盘操作
    handleKeyDown: function(e) {
      let dX = 0;
      let dY = 0;
      if (e.keyCode === 39) {
        // 右键
        dX = 1;
      } else if (e.keyCode === 37) {
        // 左键
        dX = -1;
      } else if (e.keyCode === 38) {
        // 上键
        dY = -1;
      } else if (e.keyCode === 40) {
        // 下键
        dY = 1;
      }

      if (this.draggable) {
        this.elmX += dX;
        this.elmY += dY;

        if (this.axis === "x" || this.axis === "both") {
          this.left = Math.round(this.elmX / this.grid[0]) * this.grid[0];
        }
        if (this.axis === "y" || this.axis === "both") {
          this.top = Math.round(this.elmY / this.grid[1]) * this.grid[1];
        }
        this.snapCheck();
        this.$emit("keydown", this.left, this.top);
      }
    },
    // 鼠标松开
    handleUp: function(e) {
      if (e.type.indexOf("touch") !== -1) {
        this.lastMouseX = e.changedTouches[0].clientX;
        this.lastMouseY = e.changedTouches[0].clientY;
      }
      this.handle = null;
      if (this.resizing) {
        this.resizing = false;
        this.conflictCheck(); // 冲突检测
        this.$emit("resizestop", this.left, this.top, this.width, this.height);
      }
      if (this.dragging) {
        this.dragging = false;
        this.conflictCheck(); // 冲突检测
        this.$emit("dragstop", this.left, this.top);
      }

      this.elmX = this.left;
      this.elmY = this.top;
    },
    // 将移动前的位置存储
    recording: function() {
      this.restoreY = this.top;
      this.restoreX = this.left;
      this.restoreW = this.width;
      this.restoreH = this.height;
    }
  }
};
</script>

<style scoped>
.vdr {
  position: absolute;
  box-sizing: border-box;
}
.handle {
  box-sizing: border-box;
  display: none;
  position: absolute;
  width: 4px;
  height: 4px;
  font-size: 1px;
  background: #eee;
  border: 1px solid #333;
  z-index: 999;
}
.handle-tl {
  top: -2px;
  left: -2px;
  cursor: nw-resize;
}
.handle-tm {
  top: -2px;
  left: 50%;
  margin-left: -2px;
  cursor: n-resize;
}
.handle-tr {
  top: -2px;
  right: -2px;
  cursor: ne-resize;
}
.handle-ml {
  top: 50%;
  margin-top: -2px;
  left: -2px;
  cursor: w-resize;
}
.handle-mr {
  top: 50%;
  margin-top: -2px;
  right: -2px;
  cursor: e-resize;
}
.handle-bl {
  bottom: -2px;
  left: -2px;
  cursor: sw-resize;
}
.handle-bm {
  bottom: -2px;
  left: 50%;
  margin-left: -2px;
  cursor: s-resize;
}
.handle-br {
  bottom: -2px;
  right: -2px;
  cursor: se-resize;
}
@media only screen and (max-width: 768px) {
  /* For mobile phones: */
  [class*="handle-"]:before {
    content: "";
    left: -4px;
    right: -4px;
    bottom: -4px;
    top: -4px;
    position: absolute;
  }
}
</style>
